Skip to content

Single Source of Truth

Earlier in this course, when we talked about working with forms, we saw how a form input could either be controlled or uncontrolled.

A controlled input is bound to a piece of React state:

function App() {
const [name, setName] = React.useState('');
return (
<>
<label htmlFor="name-input">
Name:
</label>
<input
id="name-input"
value={name}
onChange={(event) => {
setName(event);
}}
/>
</>
);
}

By contrast, an uncontrolled input is free to do its own thing. We might specify an initial value, but we won't control it with React state:

function App() {
return (
<>
<label htmlFor="name-input">
Name:
</label>
<input
id="name-input"
defaultValue="Enzo Matrix"
/>
</>
);
}

When we think about it, what we're really doing here is setting the “source of truth” for this input:

  • Controlled inputs use React state as the source of truth.
  • Uncontrolled inputs use the internal DOM state as the source of truth.

I'm not a browser engineer, so I don't know exactly how it works, but form controls like <input> must have some sort of internal state, managed by the browser, to track what the user has typed in.

A few years ago, my mind was blown by a realization: we can apply this same controlled/uncontrolled idea to the components that we create!

Let's consider the Counter component we saw earlier:

function Counter() {
const [count, setCount] = React.useState(0);
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
);
}

We then render this component without any props, like this:

function App() {
return (
<Counter />
);
}

From the consumer perspective, this element is totally self-contained. It has its own internal state, and I can't access it. It's like an uncontrolled input!

Now, suppose we re-write this code:

function App() {
const [count, setCount] = React.useState(0);
return (
<Counter
count={count}
onIncrement={() => setCount(count + 1)}
/>
);
}
function Counter({ count, onIncrement }) {
return (
<button onClick={onIncrement}>
{count}
</button>
);
}

In this new version, Counter is controlled by the consumer. The consumer owns the state, and we're effectively data-binding this Counter element to our state variable. Like a controlled text input!

Now, it's not exactly the same, since either way, we're using a React state variable as the source of truth, not the DOM. But from the perspective of the consumer, pretending that Counter is a black box, it's remarkably similar!

Which approach is better? Well, it depends on the circumstances! If the consumer needs to access / change the state, a controlled component makes that possible. Otherwise, it's more convenient to go with uncontrolled components.

The most important thing is that there should always be a single source of truth. We get into trouble when we start treating a component as both controlled and uncontrolled.

Spend a couple minutes considering this setup. Can you make sense of what's going on here?

Code Playground

import React from 'react';

import Thermostat from './Thermostat';
import styles from './App.module.css';

const DEFAULT_TEMP = 25;

function App() {
const [value, setValue] =
React.useState(DEFAULT_TEMP);

return (
<>
<Thermostat
value={value}
onChange={(nextValue) => {
setValue(nextValue);
}}
/>
<button
className={styles.resetBtn}
onClick={() => setValue(DEFAULT_TEMP)}
>
Reset Temperature
</button>
</>
);
}

export default App;

Let's discuss:

Video Summary

This is the same Thermostat we saw earlier, with two differences:

  1. We've removed the Celsius/Fahrenheit toggle (it's not relevant to what we're talking about today)
  2. We've added a “Reset Temperature” control outside the Thermostat component

This code is not super well-structured. Let's talk about what the problems are, and how to fix them.

Also, let's suppose that we already had a working, self-contained Thermostat component when the product/design team told us we needed to add an external button that can reset it to a pre-defined initial value, 25°.

The simplest thing might be to move the <button> into the Thermostat component, so that everything is managed internally, but let's suppose that we can't do that. The <button> is supposed to represent some sort of external control; in a real app, it might be in an entirely separate part of the page (eg. the header, a settings modal, …).

The problem is that we have two sources of truth for the temperature. We have the value state variable in App, and the temperature state variable in Thermostat. They both track the same “thing”, the current temperature.

To keep everything working, we need to make sure that these two variables are kept in sync:

  • We need an effect in the child Thermostat component that syncs temperature to value when it changes (when the “Reset Temperature” button is clicked)
  • When we increment/decrement the temperature, we pass the new value upwards, so that value is also updated

This adds significant complexity to our code, and it also makes it more bug-prone!

Suppose we comment out the onChange call:

function incrementTemperature() {
const nextTemperature = temperature + 1;
setTemperature(nextTemperature);
// onChange(nextTemperature);
}

This breaks the app in a very peculiar way: now, the “Reset Temperature” button doesn't work!

(Please see the video for a breakdown on why commenting out onChange breaks the “Reset Temperature” button, it's easier to explain in video format.)

React doesn't expect us to “sync” multiple state variables together like this. And we run into lots of problems when we try and do this.

Alright, so what's the solution? We'll need to remove one of the state variables, either value or temperature. And because we need to know the temperature in App, for the “Reset Temperature” button, we'll keep value.

Here's how this affects the Temperature component:

function Thermostat({ value, onChange }) {
- const [temperature, setTemperature] = React.useState(value);
- // Sync the `temperature` state variable
- // with the `value` prop:
- React.useEffect(() => {
- setTemperature(value);
- }, [value])
function incrementTemperature() {
+ const nextTemperature = value + 1;
- const nextTemperature = temperature + 1;
- setTemperature(nextTemperature);
onChange(nextTemperature);
}
function decrementTemperature() {
+ const nextTemperature = value - 1;
- const nextTemperature = temperature - 1;
- setTemperature(nextTemperature);
onChange(nextTemperature);
}
return (
<div className={styles.wrapper}>
<div className={styles.logo}>
Sugarfine
</div>
<div className={styles.digitalScreen}>
+ {value}°
- {temperature}°
</div>
<div className={styles.controls}>
<div className={styles.tempAdjustButtons}>
<button
className={styles.iconButton}
onClick={decrementTemperature}
>
<ChevronDown size={32} />
<VisuallyHidden>
Decrease temperature
</VisuallyHidden>
</button>
<button
className={styles.iconButton}
onClick={incrementTemperature}
>
<ChevronUp size={32} />
<VisuallyHidden>
Increase temperature
</VisuallyHidden>
</button>
</div>
</div>
</div>
);
}
export default Thermostat;

Quite a lot less code!

Essentially, the decision here is whether Temperature should be a controlled or uncontrolled component. Controlled components get their source of truth from the parent, while uncontrolled components manage their own internal state.

The components we write should always be either controlled or uncontrolled, and we should decide based on whether it's possible for them to be entirely self-contained or not.

Here's the final code from the video:

Further reading

The official React docs have a blog post, You Probably Don’t Need Derived State. It touches on a lot of these ideas!

Unfortunately, this post is from 2018, and so all of the examples use class-based components. But I think the core ideas are still just as relevant today, and hopefully it's relatively clear!